package ru.spbau.bytecode.reader;

//import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

import ru.spbau.bytecode.info.*;

public class BytecodeReader {
	
	private DataInputStream in;

	private int constantPoolCount;
	private ConstantPoolInfo cpi[];

	private int thisClassIndex;
	
	public Set<String> getClasses() {
		Set<String> paths = new HashSet<String>();
		String thisClassName = getCurrentClass();
		for (ConstantPoolInfo poolInfo: cpi) {
			if (poolInfo != null &&
					poolInfo.tag == ConstantPoolInfo.CLASS_TAG) {
				String className = getClassName((ConstantClassInfo) (poolInfo));
				if (!thisClassName.equals(className)) {
					paths.add(className);
				}
			}
		}
		
		return paths;
	}
	
	public String getCurrentClass() {
		return getClassName((ConstantClassInfo) cpi[thisClassIndex]);
	}
	
	private String getClassName(ConstantClassInfo info) {
		int classNameIndex = info.classNameIndex;
		ConstantUTF8Info utf8 = (ConstantUTF8Info) cpi[classNameIndex];
		String className = utf8.str;
		
		if (className.charAt(0) == '[') {
			className = className.substring(1);
		}
		if (className.charAt(0) == 'L') {
			className = className.substring(1);
		}
		if (className.charAt(className.length() - 1) == ';') {
			className = className.substring(0, className.length() - 1);
		}
		
		return className.replace('/', '.');
	}
	
	public void run(String path) throws IOException {
		in = new DataInputStream(new FileInputStream(path));

		in.readInt();			// read magic
		in.readUnsignedShort();	// read minor version
		in.readUnsignedShort();	// read major version

		constantPoolCount = in.readUnsignedShort();
		cpi = new ConstantPoolInfo[constantPoolCount];
		readConstantPoolInfo();

		in.readUnsignedShort();	// read accessFlags
		thisClassIndex = in.readUnsignedShort();
		in.readUnsignedShort();	// read superClassIndex 

		int interfacesCount = in.readUnsignedShort();
		int interfaces[] = new int[interfacesCount];
		for (int i = 0; i < interfacesCount; i++)
			interfaces[i] = in.readUnsignedShort();

		try {
			in.close();
		} catch (IOException e) {
			// ignore this exception while closing file
		}
	}

	private void readConstantPoolInfo() throws IOException {
		
		for (int i = 1; i < constantPoolCount; i++) {
			int tag = in.readUnsignedByte();

			switch (tag) {
			case ConstantPoolInfo.UTF8_TAG:
				int length = in.readUnsignedShort();
//				String str = in.readUTF();
				byte bytes[] = new byte[length];
				in.readFully(bytes);

				// Doesn't work...
//				 String str = DataInputStream.readUTF(new DataInputStream(
//				 new ByteArrayInputStream(bytes)));

				// It's incorrect but it works
				String str = new String(bytes);
//				System.err.println(str);

				cpi[i] = new ConstantUTF8Info(tag, str);
				break;
			case ConstantPoolInfo.INTEGER_TAG:
				int intValue = in.readInt();
				cpi[i] = new ConstantIntegerInfo(tag, intValue);
				break;
			case ConstantPoolInfo.FLOAT_TAG:
				float floatValue = in.readFloat();
				cpi[i] = new ConstantFloatInfo(tag, floatValue);
				break;
			case ConstantPoolInfo.LONG_TAG:
				int highBytes = in.readInt();
				int lowBytes = in.readInt();
				cpi[i] = new ConstantLongInfo(tag, ((long) highBytes << 32)
						+ lowBytes);
				i++;
				break;
			case ConstantPoolInfo.DOUBLE_TAG:
				highBytes = in.readInt();
				lowBytes = in.readInt();
				double value = 0; // TODO some double value from $highBytes and $lowBytes
				cpi[i] = new ConstantDoubleInfo(tag, value);
				i++;
				break;
			case ConstantPoolInfo.CLASS_TAG:
				int nameIndex = in.readUnsignedShort();
				cpi[i] = new ConstantClassInfo(tag, nameIndex);
				break;
			case ConstantPoolInfo.STRING_TAG:
				int stringIndex = in.readUnsignedShort();
				cpi[i] = new ConstantStringInfo(tag, stringIndex);
				break;
			case ConstantPoolInfo.FIELDREF_TAG:
				int classIndex = in.readUnsignedShort();
				int nameAndTypeIndex = in.readUnsignedShort();
				cpi[i] = new ConstantFieldrefInfo(tag, classIndex,
						nameAndTypeIndex);
				break;
			case ConstantPoolInfo.METHODREF_TAG:
				classIndex = in.readUnsignedShort();
				nameAndTypeIndex = in.readUnsignedShort();
				cpi[i] = new ConstantMethodrefInfo(tag, classIndex,
						nameAndTypeIndex);
				break;
			case ConstantPoolInfo.INTERFACE_METHODREF_TAG:
				classIndex = in.readUnsignedShort();
				nameAndTypeIndex = in.readUnsignedShort();
				cpi[i] = new ConstantInterfaceMethodrefInfo(tag, classIndex,
						nameAndTypeIndex);
				break;
			case ConstantPoolInfo.NAME_AND_TYPE_TAG:
				nameIndex = in.readUnsignedShort();
				int descriptorIndex = in.readUnsignedShort();
				cpi[i] = new ConstantNameAndTypeInfo(tag, nameIndex,
						descriptorIndex);
				break;
			case ConstantPoolInfo.METHOD_HANDLE_TAG:
				int refKind = in.readUnsignedByte();
				int refIndex = in.readUnsignedShort();
				cpi[i] = new ConstantMethodHandleInfo(tag, refKind, refIndex);
				break;
			case ConstantPoolInfo.METHOD_TYPE_TAG:
				descriptorIndex = in.readUnsignedShort();
				cpi[i] = new ConstantMethodTypeInfo(tag, descriptorIndex);
				break;
			case ConstantPoolInfo.INVOKE_DYNAMIC_TAG:
				int bootstrapMethodAttrIndex = in.readUnsignedShort();
				nameAndTypeIndex = in.readUnsignedShort();
				cpi[i] = new ConstantInvokeDynamicInfo(tag,
						bootstrapMethodAttrIndex, nameAndTypeIndex);
				break;
			default:
				throw new EOFException();
			}
		}
	}
}
